const { Result, Ctrl } = require('./E');
const { Device, DeviceInfo, ChargeLevel, ControlProgress } = require('./D');
const NFCUtil = require('./NFCUtil');
const AESUtil = require('./AESUtil');
const CMD = require('./CMD');

/**
 * Manages device operations and communications
 */
class DeviceManager {
  /**
   * Initialize a device connection
   * @param {NfcA} t - NFC-A tag object
   * @returns {Promise<[Symbol, Device|null]>} - A promise resolving to a pair containing function result and device object
   */
  static async init(t) {
    try {
      if (await NFCUtil.start(t)) {
        const address = await NFCUtil.getStartAddress(t);
        if (address !== null) {
          return [Result.OK, new Device(address)];
        }
      }
    } catch (err) {
      // tag lost
    }
    return [Result.ERROR, null];
  }

  /**
   * Call a device function
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {number} idx - Function index
   * @param {Array<number>} content - Content to send
   * @returns {Promise<[Symbol, Array<number>|null]>} - A promise resolving to a pair containing function result and return values
   */
  static async call(t, d, idx, content) {
    if (await NFCUtil.write(t, d.s, idx, content)) {
      const res = await NFCUtil.read(t, d.shift(1), 6);
      if (res !== null) {
        return [Result.OK, res];
      }
    }
    return [Result.ERROR, null];
  }

  /**
   * Transmit commands and check results
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {Array<Array<number>>} contents - List of content to be sent
   * @param {Function} callback - Callback function to determine if return value is valid
   * @returns {Promise<boolean>} - A promise resolving to whether all operations succeeded
   * @private
   */
  static async #transAndCheckResult(t, d, contents, callback) {
    for (let idx = 0; idx < contents.length; idx++) {
      const cmd = contents[idx];
      if (await NFCUtil.write(t, d.s, 0x00, cmd)) {
        const res = await NFCUtil.read(t, d.shift(1), 9);
        // Use unsigned right shift to ensure we're dealing with unsigned values
        if (res === null || ((res[0] >>> 16) <= 0) || !callback(res, idx)) {
          return false;
        }
      } else {
        return false;
      }
    }
    return true;
  }

  /**
   * Encrypt device key
   * @param {Uint8Array} key - Device key
   * @param {DeviceInfo} info - Device info
   * @returns {Uint8Array} - Encrypted device key
   */
  static enc(key, info) {
    const result = new Uint8Array(16);
    for (let i = 0; i < 8; i++) {
      result[i] = Number((info.id >> BigInt(8 * (7 - i))) & BigInt(0xFF));
      result[i + 8] = Number((info.id >> BigInt(8 * (7 - i))) & BigInt(0xFF));
    }
    return AESUtil.encrypt(result, key, new Uint8Array(16).fill(0));
  }

  /**
   * Get device information
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @returns {Promise<[Symbol, DeviceInfo|null]>} - A promise resolving to a pair containing function result and device info
   */
  static async getInfo(t, d) {
    let id = BigInt(0);
    let isNew = false;
    const commands = [
      CMD.ID(),
      CMD.CNT()
    ];
    
    const result = await this.#transAndCheckResult(t, d, commands, (res, idx) => {
      if (idx === 0) {
        // Convert to BigInt to handle potential large unsigned values
        id = (BigInt(res[3] >>> 0) << BigInt(32)) | BigInt(res[4] >>> 0);
      } else if (idx === 1) {
        // Use unsigned right shift to ensure we're dealing with unsigned values
        isNew = ((res[3] >>> 24) === 0);
      }
      return true;
    });
    
    if (result) {
      return [Result.OK, new DeviceInfo(id, isNew)];
    }
    return [Result.ERROR, null];
  }

  /**
   * Select user type
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {number} type - User type
   * @returns {Promise<Symbol>} - A promise resolving to the result of the operation
   */
  static async select(t, d, type) {
    const commands = [
      CMD.SEL(type),
    ];
    
    const result = await this.#transAndCheckResult(t, d, commands, () => true);
    return result ? Result.OK : Result.ERROR;
  }

  /**
   * Set device key
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {DeviceInfo} info - Device info
   * @param {Uint8Array} key - Device key
   * @param {Uint8Array} sKey - Supervisor key
   * @returns {Promise<Symbol>} - A promise resolving to the result of the operation
   */
  static async setKey(t, d, info, key, sKey) {
    const commands1 = [
      CMD.DT(sKey),
      CMD.USR(sKey, "user"),
      CMD.KEY(sKey, key),
      CMD.CHK(sKey)
    ];
    
    const command2 = [
      CMD.STO(sKey)
    ];
    
    const result1 = await this.#transAndCheckResult(t, d, commands1, (res, idx) => {
      if (idx === 3) {
        const decrypted1 = AESUtil.decryptUInt(
          res,
          1,
          res.length - 1,
          sKey.slice(0, 16),
          new Uint8Array(16).fill(0)
        );
        
        const decrypted2 = AESUtil.decryptUInt(
          decrypted1,
          3,
          6,
          key.slice(0, 16),
          new Uint8Array(16).fill(0)
        );
        
        // Convert to BigInt to handle potential large unsigned values
        const id = (BigInt(decrypted2[3] >>> 0) << BigInt(32)) | BigInt(decrypted2[4] >>> 0);
        return id === info.id;
      }
      return true;
    });
    
    const result2 = result1 && await this.#transAndCheckResult(t, d, command2, () => true);
    return result2 ? Result.OK : Result.ERROR;
  }

  /**
   * Get device charge level
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {Uint8Array} key - Device key
   * @returns {Promise<[Symbol, ChargeLevel|null]>} - A promise resolving to a pair containing function result and charge level
   */
  static async getChargeLevel(t, d, key) {
    let threshold = 0;
    let raw = 0;
    const commands = [
      CMD.CGRT(key),
      CMD.CGR(key)
    ];
    
    const result = await this.#transAndCheckResult(t, d, commands, (res, idx) => {
      const decrypted = AESUtil.decryptUInt(
        res,
        1,
        res.length - 1,
        key.slice(0, 16),
        new Uint8Array(16).fill(0)
      );
      
      if (idx === 0) {
        // Use unsigned right shift to ensure we're dealing with unsigned values
        threshold = (decrypted[3] >>> 16) & 0xFFFF;
      } else if (idx === 1) {
        // Use unsigned right shift to ensure we're dealing with unsigned values
        raw = (decrypted[3] >>> 16) & 0xFFFF;
      }
      return true;
    });
    
    if (result) {
      try {
        const chargeLevel = Math.floor((raw / threshold) * 100);
        return [Result.OK, new ChargeLevel(chargeLevel, chargeLevel >= 100)];
      } catch (err) {
        // pass
      }
    }
    return [Result.ERROR, null];
  }

  /**
   * Control device
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {Uint8Array} key - Device key
   * @param {Object} ctrl - Control command
   * @returns {Promise<Symbol>} - A promise resolving to the result of the operation
   */
  static async control(t, d, key, ctrl) {
    const commands = [
      CMD.DT(key),
      CMD.USR(key, "user"),
      CMD.ARM(key, ctrl.value),
      CMD.CTRL(key, ctrl.value)
    ];
    
    const result = await this.#transAndCheckResult(t, d, commands, () => true);
    return result ? Result.OK : Result.ERROR;
  }

  /**
   * Get control progress
   * @param {NfcA} t - NFC-A tag object
   * @param {Device} d - Device object
   * @param {Uint8Array} key - Device key
   * @returns {Promise<[Symbol, ControlProgress|null]>} - A promise resolving to a pair containing function result and control progress
   */
  static async getControlProgress(t, d, key) {
    let progress = 0;
    const commands = [
      CMD.CTRP(key)
    ];
    
    const result = await this.#transAndCheckResult(t, d, commands, (res) => {
      const decrypted = AESUtil.decryptUInt(
        res,
        1,
        res.length - 1,
        key.slice(0, 16),
        new Uint8Array(16).fill(0)
      );
      
      // Use unsigned right shift to ensure we're dealing with unsigned values
      progress = (decrypted[3] >>> 24) & 0xFF;
      return true;
    });
    
    if (result) {
      return [Result.OK, new ControlProgress(progress, progress >= 100)];
    }
    return [Result.ERROR, null];
  }
}

module.exports = DeviceManager;

